Generated code - Unit of work and field data versioning, SelfServicing

Preface

Sometimes actions on entities span a longer timeframe and / or multiple screens. It's then often impossible to start a database transaction as user-interaction during a transaction should be avoided. To track all the changes made and to persist them in one transaction can then be a tedious task. With the UnitOfWork class this can be solved. The UnitOfWork class lets you collect actions on entities or collections of entities and can perform these actions in one go. The UnitOfWork class is serializable which means it can travel accross remoting boundaries. Entities and entity collections added to the UnitOfWork class are not aware that they're added to that class, so if you decide not to continue with a given UnitOfWork instance you can simply let it get out of scope. UnitOfWork objects figure out the order in which actions have to be performed automatically: first Inserts, then Updates and then Deletes. This is controllable, see the section below about Specifying the order in which the actions are executed. It's recommended to check out the LLBLGen Pro reference manual on the UnitOfWork class to learn more about the various methods it exposes, for example how to retrieve the actual contents of the UnitOfWork object.

LLBLGen Pro also supports versioning of entity fields. This can be handy in multi-page wizards which edit portions of an entity. By utilizing the versioning capability you can rollback to a previous set of field values for a particular entity.

UnitOfWork usage: single entities

Actions collected by the UnitOfWork class are not yet performed, but are performed when the UnitOfWork's Commit() method is called. A UnitOfWork class can work with single entities or collections of entities. This paragraph discusses the UnitOfWork class with single entities. A UnitOfWork class acts like a container for UnitOfWorkElement objects which can contain an entity and are defined for a given action: save or delete (update / delete directly are also supported). You can't use the UnitOfWork to store select actions.

The UnitOfWork class works with Add methods for an entity and a given action: AddForSave() or AddForDelete(). You can specify additional parameters for the action: recursive saves and save / delete restriction filters. A delete action for a new entity is ignored. The following example illustrates both methods. First a recursive save is added and after that a delete action. The actions are not executed until Commit() is called. Commit() always expects a valid ITransaction object which is used to run persistent actions in. You can commit more than one UnitOfWork object in one transaction, simply pass the same transaction object to all Commit() calls, passing false for autoCommit. Commit() can also autocommit the transaction, if all the actions succeed, you then have to use the overload of Commit() which expects a boolean, autoCommit.

// C#
CustomerEntity newCustomer = new CustomerEntity();
// ... fill newCustomer's data
AddressEntity newAddress = new AddressEntity();
// ... fill newAddress's data
newCustomer.VisitingAddress = newAddress;
newCustomer.BillingAddress = newAddress;
UnitOfWork uow = new UnitOfWork();
// add the customer for a recursive save action.
uow.AddForSave(newCustomer, true);

ProductEntity productToDelete = new ProductEntity(productID);
// add this product for deletion.
uow.AddForDelete(newProduct);

// commit all actions in one go
uow.Commit(new Transaction(IsolationLevel.ReadCommitted, "UOW"), true);
' VB.NET
Dim newCustomer As New CustomerEntity()
' ... fill newCustomer's data
Dim newAddress As New AddressEntity()
' ... fill newAddress's data
newCustomer.VisitingAddress = newAddress
newCustomer.BillingAddress = newAddress
Dim uow As New UnitOfWork()
' add the customer for a recursive save action.
uow.AddForSave(newCustomer, True)

Dim productToDelete As New ProductEntity(productID)
' add this product for deletion.
uow.AddForDelete(newProduct)

' commit all actions in one go
uow.Commit(New Transaction(IsolationLevel.ReadCommitted, "UOW"), True)

After the Commit() action, the database has two new entities, the customer and the address, and the product entity is deleted. These actions are taken place inside a new transaction and when Commit() was called, which autocommits the transaction at the end of the actions.

UnitOfWork usage: entity collections

Sometimes a complete collection of entities has to be saved, or deleted. Instead of adding all entities individually (you could of course opt for that), you can add a collection in one go for a given action: save or delete. The following example loads an Order and its OrderDetail entities and deletes the OrderDetail entities while updating the Order entity. The entities in the collection are examined when Commit() is called. This means that an entity which is in the collection when the collection is added to the UnitOfWork object and is removed from that collection after that action but before Commit() is called, is not processed by the UnitOfWork, as the entity is no longer part of the collection being processed.

// C#
OrderEntity order = new OrderEntity(10254);
// load order detail entities through lazy loading
OrderDetailsCollection orderDetails = order.OrderDetails;
UnitOfWork uow = new UnitOfWork();
// alter order
order.EmployeeID = 3;
// add the order for save, no recursion.
uow.AddForSave(order);
uow.AddCollectionForDelete(orderDetails);
// commit all actions in one go
uow.Commit(new Transaction(IsolationLevel.ReadCommitted, "UOW"), true);
' VB.NET
Dim order As New OrderEntity(10254)
' load order detail entities through lazy loading
Dim orderDetails As OrderDetailsCollection = order.OrderDetails
Dim uow As New UnitOfWork()
' alter order
order.EmployeeID = 3
' add the order for save, no recursion.
uow.AddForSave(order)
uow.AddCollectionForDelete(orderDetails)
' commit all actions in one go
uow.Commit(New Transaction(IsolationLevel.ReadCommitted, "UOW"), True)

When Commit() is called, first all entities in the collection added are added as entities for the Delete action. After that, all actions are executed, first the save action, then the deletes.

UnitOfWork usage: stored procedures

The UnitOfWork class is able to collect calls to stored procedures as well, and lets you schedule these calls with the work already added to the UnitOfWork class, using four slots. The support for stored procedure calls is done through delegates. This means that you can use this feature also for your own methods, as long as there is a delegate defined for that method. If you want to accept the actual Transaction object, you have to make sure the method accepts an ITransaction object as the last parameter.

Adding a stored procedure call can only be done for Action procedure calls. To add a stored procedure call, you'll use the AddCallBack method, which accepts a System.Delegate object, a slot enum value which schedules the call, and zero or more parameters. Below are the slot definitions listed on which you can schedule a stored procedure call.

Enum Value Description
PreEntityInsert Execute the callback before the first entity is inserted.
PreEntityUpdate Execute the callback after the last entity has been inserted but before the first entity will be updated.
PreEntityDelete Execute the callback after the last entity has been updated but before the first entity will be deleted.
PostEntityDelete Execute the callback after the last entity has been deleted.

LLBLGen Pro generates for each Action procedure call a Delegate definition. Using such a generated delegate definition, you could add a call to a stored procedure using the following code. It adds a call to the ClearTestRunData stored procedure. It specifies that the ITransaction has to be passed into the procedure so the call will run in the same transaction as the rest of the calls the UnitOfWork object will make. If that's not done, the action procedure will create it's own transaction. The call is scheduled right before the Delete calls are made on entities.

// C#
UnitOfWork uow = new UnitOfWork();
uow.AddCallBack(new ActionProcedures.ClearTestRunDataCallBack(ActionProcedures.ClearTestRunData), 
	UnitOfWorkCallBackScheduleSlot.PreEntityDelete, true, _testRunID);
' VB.NET
Dim uow As New UnitOfWork()
uow.AddCallBack(New ActionProcedures.ClearTestRunDataCallBack(ActionProcedures.ClearTestRunData), _
	UnitOfWorkCallBackScheduleSlot.PreEntityDelete, True, _testRunID)

UnitOfWork usage: DeleteMulti and UpdateMulti

Besides adding calls to stored procedures, the UnitOfWork object can also accept calls to DeleteMulti and UpdateMulti. You add calls to one of these methods by using one of the overloads of AddDeleteMultiCall or AddUpdateMultiCall, by specifying the collection the call has to be made on and the required parameters. The calls will be executed inside the active transaction used by Commit. The DeleteMulti call will be executed after the entity delete actions but before the PostEntityDelete callbacks. The UpdateMulti call will be executed after the last entity has been updated but before the PreEntityUpdate callbacks.

UnitOfWork usage: Monitoring

In some cases you want to know what a UnitOfWork object contains, and what for example the order is in which actions will take place, for example what the real amount of entities is which will be inserted or are inserted. LLBLGen Pro's UnitOfWork offers various ways to retrieve this data. For example, the UnitOfWork classes offer you to pre-calculate the save queue, by the method ConstructSaveProcessQueues. This method constructs the save queues for insert and update, and is also used by Commit(). After this method call, you can call GetInsertQueue and GetUpdateQueue, to retrieve the exact queue of entities which will be processed during Commit for a save action. Otherwise it would be hard to figure out the exact amount, because an entity which is added using AddForSave() and specified to be saved recursively, could save more entities than itself, due to the recursive save.

These save queues are kept after Commit(). So you can also call GetInsertQueue and GetUpdateQueue after Commit() to see what happened during Commit. For deletes, you can use GetEntityElementsToDelete because Deletes are never recursive. UnitOfWork also offers methods to retrieve the elements added for insert and update, as well as other elements being added. You can then use the returned collections to for example remove an element from the UnitOfWork object. The LLBLGen Pro reference manual will show you all methods and properties available to you for information retrieval.

LLBLGen Pro uses ConstructSaveProcessQueues before a UnitOfWork object is serialized into a remoting stream, to be sure all elements to send over the wire are indeed elements which participate in a save action. You can switch this off, by setting unitOfWork2.OptimizedSerialization to false.

Specifying the order in which the actions are executed

The UnitOfWork2class typically executes the actions added to it in the following order: CallBacks for the PreEntityInsert slot, Inserts, CallBacks for the PreEntityUpdate slot, Updates, UpdateMulti calls, CallBacks for the PreEntityDelete slot, Deletes, CallBacks for the PostEntityDelete slot, DeleteMulti calls. This order can be too limited, for example if you first have to delete an entity before a new insert can take place because the entity to insert has the same value for a field with a unique constraint. LLBLGen Pro lets you define entity oriented actions to be ordered in a different order, so the UnitOfWork class will for example first execute the deletes and then the updates. This is done by specifying a list of UnitOfWorkBlockType values for the property unitOfWork.CommitOrder. By default you don't have to specify any commit order, the UnitOfWork class will follow the sequence as specified above. However as soon as you specify a list of UnitOfWorkBlockType values for CommitOrder, it will use that list instead. This means that if you omit a block type, these actions aren't executed at all. Duplicates are filtered out so specifying a blocktype twice has no effect, the second one is ignored.

CallBacks with the name Preaction or Postaction belong to the blocktype of action and will be executed in that block, in the same order as described above, so for example PreUpdateEntity callbacks are executed before the updates, when the blocktype for updates is specified to be executed.

Field data versioning

One innovative feature of LLBLGen Pro is its field data versioning. The fields of an entity, say a CustomerEntity, can be versioned and saved under a name inside the entity object itself. Later, you can decide to rollback the entity's field values at a later time. The versioned field data is contained inside the entity and can pass with the entity remoting borders and is saved inside the XML produced by WriteXml(). All fields are versioned at once, you can't version a field's values individually.

The following example loads an entity, saves its field values, alters them and then rolls them back, when an exception occurs.

// C#
CustomerEntity customer = new CustomerEntity("CHOPS");
customer.SaveFields("BeforeUpdate");
try
{
	// show a form to the user which allows the user to
	// edit the customer entity
	ShowModifyCustomerForm(customer);
}
catch
{
	// something went wrong. Entity can be altered. Roll back
	// fields so further processing won't be affected by these
	// changes which are not completed
	customer.RollbackFields("BeforeUpdate");
	throw;
}
' VB.NET
Dim customer As New CustomerEntity("CHOPS")
customer.SaveFields("BeforeUpdate")
Try
	' show a form to the user which allows the user to
	' edit the customer entity
	ShowModifyCustomerForm(customer)
Catch
	' something went wrong. Entity can be altered. Roll back
	' fields so further processing won't be affected by these
	' changes which are not completed
	customer.RollbackFields("BeforeUpdate")
	Throw
End Try


LLBLGen Pro v3.0 documentation. ©2010 Solutions Design